package asava.tools;

import asava.sources.Source;
import asava.utils.Position;

import java.io.IOException;

import static asava.tools.Tokens.*;

/**
 * @author Tim Richter
 */
public final class Scanner {
    private static final char EOFCHAR = '\032';

    /**
     * @param c    character
     * @param base numeric base
     * @return the numeric representation of {@code c}
     */
    public static int charToDigit(final char c, final int base) {
        if (c >= '0' && c <= '9' && c < '0' + base) {
            return c - '0';
        } else if (c >= 'a' && c < 'a' + base - 10) {
            return c - 'a' + 10;
        } else if (c >= 'A' && c <= 'A' + base - 10) {
            return c - 'A' + 10;
        }
        return -1;
    }

    /**
     * identStart := [A..Z] | [a..z] | '_'
     *
     * @param c character
     * @return true, if {@code c} is a start of an identifier
     */
    public static boolean isIdentifierStart(final char c) {
        return c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z' || c == '_';
    }

    /**
     * identPart := identStart | digit
     *
     * @param c character
     * @return true, if {@code c} is a part of an identifier
     */
    public static boolean isIdentifierPart(final char c) {
        return isIdentifierStart(c) || isDigit(c);
    }

    /**
     * digit := [0..9]
     *
     * @param c character
     * @return true, if {@code c} is a digit
     */
    public static boolean isDigit(final char c) {
        return c >= '0' && c <= '9';
    }

    /**
     * hexdigit := digit | [A..F] | [a..f]
     *
     * @param c character
     * @return true, if {@code c} is a hex digit
     */
    public static boolean isHexDigit(final char c) {
        return isDigit(c) || c >= 'A' && c <= 'F' || c >= 'a' && c <= 'f';
    }

    /* reports errors during scanning process */
    private final Reporter reporter;
    /* source of the buffer */
    private Source source;
    /* char buffer */
    private char[] buf;
    /* current char */
    private char cc;
    /* start of the token source */
    private int start;
    /* end of the token source */
    private int end;
    /* current line */
    private int line;
    /* current column */
    private int column;
    /* flag whether the current token is the first one in a new line */
    private boolean nl;
    /* buffer for string literals */
    private final StringBuilder stringBuffer;

    public Scanner(final Reporter reporter) {
        this.reporter = reporter;
        this.stringBuffer = new StringBuilder();
    }

    /**
     * sets the source to be scanned by this scanner.
     *
     * @param source source
     */
    public void source(final Source source) {
        this.source = source;
        try {
            this.buf = source().content();
            this.start = 0;
            this.end = 0;
            this.line = 0;
            this.column = 0;
            this.cc = buf[start];
        } catch (IOException e) {
            error("cannot read source");
        }
    }

    /**
     * @return the source currently scanned by this scanner
     */
    public Source source() {
        if (source == null) {
            throw new IllegalStateException();
        } else {
            return source;
        }
    }

    /**
     * @return the start index of the current token source
     */
    public int start() {
        return start;
    }

    /**
     * @return the end index of the current token source
     */
    public int end() {
        return end;
    }

    /**
     * @return true, if the current token is the first in a new line
     */
    public boolean nl() {
        return nl;
    }

    /**
     * @return the current position of the scanner
     */
    public Position position() {
        return new Position(source, line, column, start, end);
    }

    /**
     * @param start start index
     * @param end   end index
     * @return the string representation of the token source
     */
    public String tokenSource(final int start, final int end) {
        return new String(buf, start, end - start);
    }

    /**
     * @return the string representation of the token source
     */
    public String tokenSource() {
        return new String(buf, start, end - start);
    }

    /**
     * @return the string literal of the current token source
     */
    public String stringLiteral() {
        final String source = stringBuffer.toString();
        stringBuffer.delete(0, stringBuffer.length());
        return source;
    }

    /**
     * @return the int literal of the current token source
     */
    public int intLiteral() {
        int len = end - start;
        int off = start;
        int rad = 10;
        int res = 0;
        int max = Integer.MAX_VALUE;
        char c = buf[off];

        if (c == '0' && len > 1) {
            c = buf[off + 1];
            if (c == 'x' || c == 'X') {
                rad = 16;
                off += 2;
            } else {
                rad = 8;
            }
        }

        while (off < len) {
            int d = charToDigit(buf[off++], rad);
            if (d < 0) {
                error("malformed integer literal");
                return 0;
            }
            res *= rad;
            if (res < max + d) {
                error("integer literal too big");
                break;
            }
            res -= d;
        }
        return -res;
    }

    /**
     * @return the number literal of the current token source
     */
    public double numberLiteral() {
        try {
            return Double.valueOf(tokenSource());
        } catch (final NumberFormatException e) {
            error("malformed float literal");
            return 0;
        }
    }

    /**
     * @return the next token scanned ({@code Token.EOF} if the end of the file was reached)
     * @see Tokens
     */
    public int nextToken() {
        // sentinel token
        if (cc == EOFCHAR) {
            return EOF;
        }
        // reset new line flag
        nl = false;
        // consume whitespace and comments
        whitespaceAndComments();
        // reset start position for the next token source
        start = end;

        switch (cc) {
            case '0':
            case '1':
            case '2':
            case '3':
            case '4':
            case '5':
            case '6':
            case '7':
            case '8':
            case '9':
                return numericToken();
            case '"':
            case '\'':
                return stringToken();
            case '.':
                if (isDigit(nextChar())) {
                    return numericToken();
                } else if (cc == DOT) {
                    if (nextChar() == DOT) {
                        return DOTDOTDOT;
                    }
                    revert(1);
                }
                return DOT;
            case ',':
                nextChar();
                return COMMA;
            case ';':
                nextChar();
                return SEMI;
            case ':':
                if (nextChar() == ':') {
                    nextChar();
                    return COLONCOLON;
                }
                return COLON;
            case '?':
                return QUES;
            case '(':
                return LPAREN;
            case ')':
                return RPAREN;
            case '{':
                return LBRACE;
            case '}':
                return RBRACE;
            case '[':
                return LBRACK;
            case ']':
                return RBRACK;
            case '=':
                if (nextChar() == '=') {
                    if (nextChar() == '=') {
                        nextChar();
                        return EQEQEQ;
                    }
                    return EQEQ;
                }
                return EQ;
            case '!':
                if (nextChar() == '=') {
                    if (nextChar() == '=') {
                        nextChar();
                        return NOTEQEQ;
                    }
                    return NOTEQ;
                }
                return NOT;
            case '+':
                if (nextChar() == '=') {
                    nextChar();
                    return PLUSEQ;
                } else if (cc == '+') {
                    nextChar();
                    return PLUSPLUS;
                }
                return PLUS;
            case '-':
                if (nextChar() == '=') {
                    nextChar();
                    return MINEQ;
                } else if (cc == '+') {
                    nextChar();
                    return MINMIN;
                }
                return MIN;
            case '/':
                if (nextChar() == '=') {
                    nextChar();
                    return SLASHEQ;
                }
                return SLASH;
            case '*':
                if (nextChar() == '=') {
                    nextChar();
                    return STAREQ;
                }
                return STAR;
            case '%':
                if (nextChar() == '=') {
                    nextChar();
                    return PERCEQ;
                }
                return PERC;
            case '^':
                if (nextChar() == '=') {
                    nextChar();
                    return CARETEQ;
                }
                return CARET;
            case '&':
                if (nextChar() == '=') {
                    nextChar();
                    return AMPEQ;
                } else if (cc == '&') {
                    nextChar();
                    return AMPAMP;
                }
                return AMP;
            case '|':
                if (nextChar() == '=') {
                    nextChar();
                    return BAREQ;
                } else if (cc == '|') {
                    nextChar();
                    return BARBAR;
                }
                return BAR;
            case '~':
                return TILDE;
            case '<':
                if (nextChar() == '=') {
                    nextChar();
                    return LTEQ;
                } else if (cc == '<') {
                    if (nextChar() == '=') {
                        nextChar();
                        return LTLTEQ;
                    }
                    return LTLT;
                }
                return LT;
            case '>':
                if (nextChar() == '=') {
                    nextChar();
                    return GTEQ;
                } else if (cc == '>') {
                    if (nextChar() == '=') {
                        nextChar();
                        return GTGTEQ;
                    } else if (cc == '>') {
                        if (nextChar() == '=') {
                            nextChar();
                            return GTGTGTEQ;
                        }
                        return GTGTGT;
                    }
                    return GTGT;
                }
                return GT;
            case 'a':
                if (nextChar() == 's' && !isIdentifierPart(nextChar())) {
                    return AS;
                }
                break;
            case 'b':
                if (nextChar() == 'r' && nextChar() == 'e' && nextChar() == 'a' && nextChar() == 'k' && !isIdentifierPart(nextChar())) {
                    return BREAK;
                }
                break;
            case 'c':
                if (nextChar() == 'a') {
                    if (nextChar() == 's') {
                        if (nextChar() == 'e' && !isIdentifierPart(nextChar())) {
                            return CASE;
                        }
                    } else if (cc == 't' && nextChar() == 'c' && nextChar() == 'h' && !isIdentifierPart(nextChar())) {
                        return CATCH;
                    }
                } else if (cc == 'l') {
                    if (nextChar() == 'a' && nextChar() == 's' && nextChar() == 's' && !isIdentifierPart(nextChar())) {
                        return CLASS;
                    }
                } else if (cc == 'o') {
                    if (nextChar() == 'n') {
                        if (nextChar() == 's') {
                            if (nextChar() == 't' && !isIdentifierPart(nextChar())) {
                                return CONST;
                            }
                        } else if (cc == 't' && nextChar() == 'i' && nextChar() == 'n' && nextChar() == 'u' && nextChar() == 'e' && !isIdentifierStart(nextChar())) {
                            return CONTINUE;
                        }
                    }
                }
                break;
            case 'd':
                if (nextChar() == 'e') {
                    if (nextChar() == 'f') {
                        if (nextChar() == 'a' && nextChar() == 'u' && nextChar() == 'l' && nextChar() == 't' && !isIdentifierPart(nextChar())) {
                            return DEFAULT;
                        }
                    } else if (cc == 'l' && nextChar() == 'e' && nextChar() == 't' && nextChar() == 'e' && !isIdentifierPart(nextChar())) {
                        return DELETE;
                    }
                } else if (cc == 'o') {
                    if (!isIdentifierPart(nextChar())) {
                        return DO;
                    }
                } else if (cc == 'y' && nextChar() == 'n' && nextChar() == 'a' && nextChar() == 'm' && nextChar() == 'i' && nextChar() == 'c' && !isIdentifierPart(nextChar())) {
                    return DYNAMIC;
                }
                break;
            case 'e':
                if (nextChar() == 'a') {
                    if (nextChar() == 'c' && nextChar() == 'h' && !isIdentifierPart(nextChar())) {
                        return EACH;
                    }
                } else if (cc == 'l') {
                    if (nextChar() == 's' && nextChar() == 'e' && !isIdentifierPart(nextChar())) {
                        return ELSE;
                    }
                } else if (cc == 'x' && nextChar() == 't' && nextChar() == 'e' && nextChar() == 'n' && nextChar() == 'd' && nextChar() == 's' && !isIdentifierPart(nextChar())) {
                    return EXTENDS;
                }
                break;
            case 'f':
                if (nextChar() == 'a') {
                    if (nextChar() == 'l' && nextChar() == 's' && nextChar() == 'e' && !isIdentifierPart(nextChar())) {
                        return FALSE;
                    }
                } else if (cc == 'i') {
                    if (nextChar() == 'n' && nextChar() == 'a' && nextChar() == 'l') {
                        if (!isIdentifierPart(nextChar())) {
                            return FINAL;
                        } else if (cc == 'l' && nextChar() == 'y' && !isIdentifierPart(nextChar())) {
                            return FINALLY;
                        }
                    }
                } else if (cc == 'o') {
                    if (nextChar() == 'r' && !isIdentifierPart(nextChar())) {
                        return FOR;
                    }
                } else if (cc == 'u' && nextChar() == 'n' && nextChar() == 'c' && nextChar() == 't' && nextChar() == 'i' && nextChar() == 'o' && nextChar() == 'n' && !isIdentifierPart(nextChar())) {
                    return FUNCTION;
                }
                break;
            case 'g':
                if (nextChar() == 'e' && nextChar() == 't' && !isIdentifierPart(nextChar())) {
                    return GET;
                }
                break;
            case 'i':
                if (nextChar() == 'f') {
                    if (!isIdentifierPart(nextChar())) {
                        return IF;
                    }
                } else if (cc == 'm') {
                    if (nextChar() == 'p') {
                        if (nextChar() == 'l') {
                            if (nextChar() == 'e' && nextChar() == 'm' && nextChar() == 'e' && nextChar() == 'n' && nextChar() == 't' && nextChar() == 's' && !isIdentifierPart(nextChar())) {
                                return IMPLEMENTS;
                            }
                        } else if (cc == 'o' && nextChar() == 'r' && nextChar() == 't' && !isIdentifierPart(nextChar())) {
                            return IMPORT;
                        }
                    }
                } else if (cc == 'n') {
                    if (!isIdentifierPart(nextChar())) {
                        return IN;
                    } else if (cc == 's') {
                        if (nextChar() == 't' && nextChar() == 'a' && nextChar() == 'n' && nextChar() == 'c' && nextChar() == 'e' && nextChar() == 'o' && nextChar() == 'f' && !isIdentifierPart(nextChar())) {
                            return INSTANCEOF;
                        }
                    } else if (cc == 't' && nextChar() == 'e' && nextChar() == 'r') {
                        if (nextChar() == 'f') {
                            if (nextChar() == 'a' && nextChar() == 'c' && nextChar() == 'e' && !isIdentifierPart(nextChar())) {
                                return INTERFACE;
                            }
                        } else if (cc == 'n' && nextChar() == 'a' && nextChar() == 'l' && !isIdentifierPart(nextChar())) {
                            return INTERNAL;
                        }
                    }
                } else if (cc == 's' && !isIdentifierPart(nextChar())) {
                    return IS;
                }
                break;
            case 'n':
                if (nextChar() == 'a') {
                    if (nextChar() == 'm') {
                        if (nextChar() == 'e' && nextChar() == 's' && nextChar() == 'p' && nextChar() == 'a' && nextChar() == 'c' && nextChar() == 'e' && !isIdentifierPart(nextChar())) {
                            return NAMESPACE;
                        }
                    } else if (cc == 't' && nextChar() == 'i' && nextChar() == 'v' && nextChar() == 'e' && !isIdentifierPart(nextChar())) {
                        return NATIVE;
                    }
                } else if (cc == 'e') {
                    if (nextChar() == 'w' && !isIdentifierPart(nextChar())) {
                        return NEW;
                    }
                } else if (cc == 'u' && nextChar() == 'l' && nextChar() == 'l' && !isIdentifierPart(nextChar())) {
                    return NULL;
                }
                break;
            case 'o':
                if (nextChar() == 'v' && nextChar() == 'e' && nextChar() == 'r' && nextChar() == 'r' && nextChar() == 'i' && nextChar() == 'd' && nextChar() == 'e' && !isIdentifierPart(nextChar())) {
                    return OVERRIDE;
                }
                break;
            case 'p':
                if (nextChar() == 'a') {
                    if (nextChar() == 'c' && nextChar() == 'k' && nextChar() == 'a' && nextChar() == 'g' && nextChar() == 'e' && !isIdentifierPart(nextChar())) {
                        return PACKAGE;
                    }
                } else if (cc == 'r') {
                    if (nextChar() == 'i') {
                        if (nextChar() == 'v' && nextChar() == 'a' && nextChar() == 't' && nextChar() == 'e' && !isIdentifierPart(nextChar())) {
                            return PRIVATE;
                        }
                    } else if (cc == 'o' && nextChar() == 't' && nextChar() == 'e' && nextChar() == 'c' && nextChar() == 't' && nextChar() == 'e' && nextChar() == 'd' && !isIdentifierPart(nextChar())) {
                        return PROTECTED;
                    }
                } else if (cc == 'u' && nextChar() == 'b' && nextChar() == 'l' && nextChar() == 'i' && nextChar() == 'c' && !isIdentifierPart(nextChar())) {
                    return PUBLIC;
                }
                break;
            case 'r':
                if (nextChar() == 'e' && nextChar() == 't' && nextChar() == 'u' && nextChar() == 'r' && nextChar() == 'n' && !isIdentifierPart(nextChar())) {
                    return RETURN;
                }
                break;
            case 's':
                if (nextChar() == 'e') {
                    if (nextChar() == 't' && !isIdentifierPart(nextChar())) {
                        return SET;
                    }
                } else if (cc == 't') {
                    if (nextChar() == 'a' && nextChar() == 't' && nextChar() == 'i' && nextChar() == 'c' && !isIdentifierPart(nextChar())) {
                        return STATIC;
                    }
                } else if (cc == 'u') {
                    if (nextChar() == 'p' && nextChar() == 'e' && nextChar() == 'r' && !isIdentifierPart(nextChar())) {
                        return SUPER;
                    }
                } else if (cc == 'w' && nextChar() == 'i' && nextChar() == 't' && nextChar() == 'c' && nextChar() == 'h' && !isIdentifierPart(nextChar())) {
                    return SWITCH;
                }
                break;
            case 't':
                if (nextChar() == 'h') {
                    if (nextChar() == 'i') {
                        if (nextChar() == 's' && !isIdentifierPart(nextChar())) {
                            return THIS;
                        }
                    } else if (cc == 'r' && nextChar() == 'o' && nextChar() == 'w' && !isIdentifierPart(nextChar())) {
                        return THROW;
                    }
                } else if (cc == 'r') {
                    if (nextChar() == 'e') {
                        if (!isIdentifierPart(nextChar())) {
                            return TRUE;
                        }
                    } else if (cc == 'y' && !isIdentifierPart(nextChar())) {
                        return TRY;
                    }
                }
                break;
            case 'u':
                if (nextChar() == 's' && nextChar() == 'e' && !isIdentifierPart(nextChar())) {
                    return USE;
                }
                break;
            case 'v':
                if (nextChar() == 'a' && nextChar() == 'r' && !isIdentifierPart(nextChar())) {
                    return VAR;
                }
                break;
            case 'w':
                if (nextChar() == 'h') {
                    if (nextChar() == 'i' && nextChar() == 'l' && nextChar() == 'e' && !isIdentifierPart(nextChar())) {
                        return WHILE;
                    }
                } else if (cc == 'i' && nextChar() == 't' && nextChar() == 'h' && !isIdentifierPart(nextChar())) {
                    return WITH;
                }
                break;
        }
        // scan identifier
        while (isIdentifierPart(cc)) {
            nextChar();
        }
        return IDENT;
    }

    /**
     * consumes whitespace and comments.
     */
    private void whitespaceAndComments() {
        while (cc != EOFCHAR) {
            switch (cc) {
                case ' ':
                case '\t':
                    nextChar();
                    break;
                case '\n':
                case '\r':
                    newLine();
                    break;
                case '/':
                    comment();
                    break;
                default:
                    return;
            }
        }
    }

    /**
     * consumes single/multi line comments.
     */
    private void comment() {
        if (cc == '/') {
            if (nextChar() == '/') {
                // single line comment
                while (nextChar() != EOFCHAR) {
                    switch (cc) {
                        case '\n':
                        case '\r':
                            newLine();
                            return;
                    }
                }
            } else if (cc == '*') {
                // multi line comment
                while (nextChar() == EOFCHAR) {
                    switch (cc) {
                        case '\n':
                        case '\r':
                            newLine();
                            break;
                        case '*':
                            if (nextChar() == '/') {
                                nextChar();
                                return;
                            }
                            break;
                    }
                }
            } else {
                // revert the last 2 characters
                revert(2);
            }
        }
    }

    /**
     * consumes new line characters.
     */
    private void newLine() {
        switch (cc) {
            case '\r':
                if (nextChar() == '\n') {
                    nextChar();
                }
                break;
            case '\n':
                nextChar();
                break;
            default:
                return;
        }
        nl = true;
        column = 0;
        line++;
    }

    /**
     * consumes numeric literals.
     *
     * @return {@code Token.INT} or {@code Token.NUMBER}
     */
    private int numericToken() {
        while (isDigit(cc)) {
            nextChar();
        }
        switch (cc) {
            case '.':
                if (isDigit(nextChar())) {
                    while (isDigit(nextChar())) ;
                    if (cc == 'e' || cc == 'E') {
                        nextChar();
                        if (cc == '+' || cc == '-') {
                            nextChar();
                        }
                        while (isDigit(nextChar())) ;
                    }
                    return NUMBER;
                }
                revert(2);
                return INT;
            case 'x':
            case 'X':
                while (isHexDigit(nextChar())) ;
                return INT;
            case 'e':
            case 'E':
                nextChar();
                if (cc == '+' || cc == '-') {
                    nextChar();
                }
                while (isDigit(nextChar())) ;
                return INT;
            default:
                return INT;
        }
    }

    /**
     * consumes string literals.
     *
     * @return {@code Token.STRING}
     */
    private int stringToken() {
        final char enclosingChar = cc;
        while (nextChar() != EOFCHAR && cc != enclosingChar) {
            if (cc == '\\') {
                switch (nextChar()) {
                    case 'n':
                        stringBuffer.append("\n");
                        break;
                    case 'r':
                        stringBuffer.append("\r");
                        break;
                    case 't':
                        stringBuffer.append("\t");
                        break;
                    case 'b':
                        stringBuffer.append("\b");
                        break;
                    case 'f':
                        stringBuffer.append("\f");
                        break;
                    case 'u':
                        int cuc, code = 0;
                        while (nextChar() != EOFCHAR && cc != 'u') {
                            cuc = charToDigit(cc, 10);
                            if (cuc < 0) {
                                error("invalid escaping sequence");
                                return STRING;
                            }
                            code = (code << 4) + cuc;
                        }
                        stringBuffer.append((char) code);
                        break;
                }
            } else {
                stringBuffer.append(cc);
            }
        }
        return STRING;
    }

    /**
     * @return the next char of the buffer
     */
    private char nextChar() {
        if (end == buf.length) {
            cc = EOFCHAR;
        } else {
            cc = buf[end];
            end++;
            column++;
        }
        return cc;
    }

    /**
     * moves the head of the buffer by {@code count} characters backwards.
     *
     * @param count count
     */
    private void revert(final int count) {
        if (end - count < 0) {
            throw new IndexOutOfBoundsException("offset must be greater than zero");
        } else {
            end -= count;
            nextChar();
        }
    }

    /**
     * reports an error.
     *
     * @param message message
     */
    private void error(final String message) {
        reporter.error(position(), message);
    }
}
